# frozen_string_literal: true

require 'spec_helper'

RSpec.describe API::VulnerabilityFindings, feature_category: :vulnerability_management do
  include AccessMatchersForRequest

  let_it_be(:project) { create(:project, :public) }
  let_it_be(:user) { create(:user) }

  describe 'GET /projects/:id/vulnerability_findings' do
    let(:project_vulnerability_findings_path) { "/projects/#{project.id}/vulnerability_findings" }

    let_it_be(:pipeline) { create(:ci_empty_pipeline, status: :created, project: project) }
    let_it_be(:pipeline_without_vulnerabilities) { create(:ci_pipeline, status: :created, project: project) }

    let_it_be(:build_ds) { create(:ci_build, :success, name: 'ds_job', pipeline: pipeline, project: project) }
    let_it_be(:build_sast) { create(:ci_build, :success, name: 'sast_job', pipeline: pipeline, project: project) }

    let(:ds_report) { pipeline.security_reports.reports['dependency_scanning'] }
    let(:sast_report) { pipeline.security_reports.reports['sast'] }

    before_all do
      create(:security_scan, :latest_successful, :with_findings, scan_type: :dependency_scanning, build: build_ds)
      create(:security_scan, :latest_successful, :with_findings, scan_type: :sast, build: build_sast)
    end

    before do
      stub_licensed_features(security_dashboard: true, sast: true, dependency_scanning: true, container_scanning: true)

      create(:vulnerability_feedback, :dismissal, :sast,
             project: project,
             pipeline: pipeline,
             project_fingerprint: sast_report.findings.first.project_fingerprint,
             vulnerability_data: sast_report.findings.first.raw_metadata,
             finding_uuid: sast_report.findings.first.uuid
      )

      finding = create(:vulnerabilities_finding, :dismissed, uuid: sast_report.findings.first.uuid)

      vulnerability = finding.vulnerability
      issue = create(:issue, project: project)
      merge_request = create(:merge_request, source_project: project)

      create(:vulnerabilities_issue_link, issue: issue, vulnerability: vulnerability)
      create(:vulnerabilities_merge_request_link, merge_request: merge_request, vulnerability: vulnerability)
      create(:vulnerability_state_transition, vulnerability: vulnerability)
    end

    context 'with an authorized user with proper permissions' do
      before do
        create(:vulnerability_statistic, project: project, pipeline: pipeline)
        project.add_developer(user)
      end

      # Because fixture reports that power :ee_ci_job_artifact factory contain long report lists,
      # we need to make sure that all findings for both SAST and Dependency Scanning are included in the response.
      # That's why the page size is 40.
      let(:pagination) { { per_page: 40 } }

      it 'returns all non-dismissed vulnerabilities', :aggregate_failures do
        # all findings except one that was dismissed
        finding_count = (sast_report.findings.count + ds_report.findings.count - 1).to_s

        get api(project_vulnerability_findings_path, user), params: pagination
        expect(response).to have_gitlab_http_status(:ok)
        expect(response).to include_pagination_headers
        expect(response).to match_response_schema('vulnerabilities/finding_list', dir: 'ee')

        expect(response.headers['X-Total']).to eq finding_count

        expect(json_response.map { |v| v['report_type'] }.uniq).to match_array %w[dependency_scanning sast]
      end

      describe 'using different finders' do
        let(:mock_findings_finder) { instance_double(Security::FindingsFinder, execute: []) }
        let(:mock_pure_findings_finder) do
          instance_double(Security::PureFindingsFinder,
                          execute: [], available?: pure_finder_available?)
        end

        before do
          allow(Security::FindingsFinder).to receive(:new).and_return(mock_findings_finder)
          allow(Security::PureFindingsFinder).to receive(:new).and_return(mock_pure_findings_finder)

          get api(project_vulnerability_findings_path, user), params: pagination
        end

        context 'when the `Security::PureFindingsFinder` is not available' do
          let(:pure_finder_available?) { false }

          it 'uses the `Security::FindingsFinder`', :aggregate_failures do
            expect(mock_pure_findings_finder).not_to have_received(:execute)
            expect(mock_findings_finder).to have_received(:execute)
          end
        end

        context 'when the `Security::PureFindingsFinder` is available' do
          let(:pure_finder_available?) { true }

          it 'uses the `Security::FindingsFinder`', :aggregate_failures do
            expect(mock_pure_findings_finder).to have_received(:execute)
            expect(mock_findings_finder).not_to have_received(:execute)
          end
        end
      end

      describe 'filtering' do
        it 'returns vulnerabilities with sast report_type', :aggregate_failures do
          finding_count = (sast_report.findings.count - 1).to_s # all SAST findings except one that was dismissed

          get api(project_vulnerability_findings_path, user), params: { report_type: 'sast' }

          expect(response).to have_gitlab_http_status(:ok)

          expect(response.headers['X-Total']).to eq finding_count

          expect(json_response.map { |v| v['report_type'] }.uniq).to match_array %w[sast]

          # findings are implicitly sorted by Security::PipelineVulnerabilitiesFinder and
          # Security::MergeReportsService so their order differs from what is present in fixture file
          expect(json_response.first['name']).to eq 'ECB mode is insecure'
        end

        it 'returns vulnerabilities with dependency_scanning report_type', :aggregate_failures do
          finding_count = ds_report.findings.count.to_s

          get api(project_vulnerability_findings_path, user), params: { report_type: 'dependency_scanning' }

          expect(response).to have_gitlab_http_status(:ok)

          expect(response.headers['X-Total']).to eq finding_count

          expect(json_response.map { |v| v['report_type'] }.uniq).to match_array %w[dependency_scanning]

          # findings are implicitly sorted by Security::PipelineVulnerabilitiesFinder and
          # Security::MergeReportsService so their order differs from what is present in fixture file
          expect(json_response.first['name']).to eq 'ffi - CVE-2018-1000201'
        end

        it 'returns a "bad request" response for an unknown report type' do
          get api(project_vulnerability_findings_path, user), params: { report_type: 'blah' }

          expect(response).to have_gitlab_http_status(:bad_request)
        end

        it 'returns dismissed vulnerabilities with `all` scope', :aggregate_failures do
          finding_count = (sast_report.findings.count + ds_report.findings.count).to_s

          get api(project_vulnerability_findings_path, user), params: { scope: 'all' }.merge(pagination)

          expect(response).to have_gitlab_http_status(:ok)

          expect(response.headers['X-Total']).to eq finding_count
        end

        it 'returns vulnerabilities with low severity', :aggregate_failures do
          get api(project_vulnerability_findings_path, user), params: { severity: 'low' }.merge(pagination)

          expect(response).to have_gitlab_http_status(:ok)

          expect(json_response.map { |v| v['severity'] }.uniq).to eq %w[low]
        end

        it 'returns a "bad request" response for an unknown severity value' do
          get api(project_vulnerability_findings_path, user), params: { severity: 'foo' }

          expect(response).to have_gitlab_http_status(:bad_request)
        end

        it 'returns vulnerabilities with high confidence', :aggregate_failures do
          get api(project_vulnerability_findings_path, user), params: { confidence: 'high' }.merge(pagination)

          expect(response).to have_gitlab_http_status(:ok)

          expect(json_response.map { |v| v['confidence'] }.uniq).to eq %w[high]
        end

        it 'returns a "bad request" response for an unknown confidence value' do
          get api(project_vulnerability_findings_path, user), params: { confidence: 'qux' }

          expect(response).to have_gitlab_http_status(:bad_request)
        end

        context 'when pipeline_id is supplied' do
          it 'returns vulnerabilities from supplied pipeline', :aggregate_failures do
            finding_count = (sast_report.findings.count + ds_report.findings.count - 1).to_s

            get api(project_vulnerability_findings_path, user), params: { pipeline_id: pipeline.id }.merge(pagination)

            expect(response).to have_gitlab_http_status(:ok)

            expect(response.headers['X-Total']).to eq finding_count
          end

          context 'pipeline has no reports' do
            it 'returns empty results' do
              get api(project_vulnerability_findings_path, user), params: { pipeline_id: pipeline_without_vulnerabilities.id }.merge(pagination)

              expect(json_response).to eq []
            end
          end

          context 'with unknown pipeline' do
            it 'returns empty results' do
              get api(project_vulnerability_findings_path, user), params: { pipeline_id: 0 }.merge(pagination)

              expect(json_response).to eq []
            end
          end
        end
      end

      context 'when security dashboard feature is not available' do
        before do
          stub_licensed_features(security_dashboard: false)
        end

        it 'responds with 403 Forbidden' do
          get api(project_vulnerability_findings_path, user)

          expect(response).to have_gitlab_http_status(:forbidden)
        end
      end
    end

    describe 'permissions', :enable_admin_mode do
      subject(:get_vulnerability_findings) { get api(project_vulnerability_findings_path, user) }

      it { expect { get_vulnerability_findings }.to be_allowed_for(:admin) }
      it { expect { get_vulnerability_findings }.to be_allowed_for(:owner).of(project) }
      it { expect { get_vulnerability_findings }.to be_allowed_for(:maintainer).of(project) }
      it { expect { get_vulnerability_findings }.to be_allowed_for(:developer).of(project) }
      it { expect { get_vulnerability_findings }.to be_allowed_for(:auditor) }

      it { expect { get_vulnerability_findings }.to be_denied_for(:reporter).of(project) }
      it { expect { get_vulnerability_findings }.to be_denied_for(:guest).of(project) }
      it { expect { get_vulnerability_findings }.to be_denied_for(:anonymous) }
    end
  end
end
